package com.zhx.common.common.service.rateLimit;

import cn.hutool.extra.spring.SpringUtil;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;

import java.util.Collections;
import java.util.Objects;
import java.util.concurrent.TimeUnit;

/**
 * 固定窗口限流算法
 * 在指定周期内累加访问次数，当访问次数达到设定的阈值时，触发限流策略，当进入下一个时间周期时进行访问次数的清零。
 * 缺点：
 * 1.限流不够平滑。例如：限流是每秒3个，在第一毫秒发送了3个请求，达到限流，窗口剩余时间的请求都将会被拒绝，体验不好。
 * 2.无法处理窗口边界问题。因为是在某个时间窗口内进行流量控制，所以可能会出现窗口边界效应，即在时间窗口的边界处可能会有大量的请求被允许通过，
 * 从而导致突发流量。即：如果第2到3秒内产生了150次请求，而第3到4秒内产生了150次请求，那么其实在第2秒到第4秒这两秒内，就已经发生了300次请求了，
 * 远远大于我们要求的3秒内的请求不要超过150次这个限制
 */
public class FixedWindowStrategyHandler implements RateLimitStrategyHandler {

    private static final StringRedisTemplate stringRedisTemplate;


    // 定义一个常量，用于存储限流脚本
    private static final DefaultRedisScript<Long> LIMIT_SCRIPT;

    // 静态初始化块，用于加载并配置限流脚本
    static {
        stringRedisTemplate = SpringUtil.getBean(StringRedisTemplate.class);
        // 初始化Redis脚本，设置脚本位置和结果类型
        LIMIT_SCRIPT = new DefaultRedisScript<>();
        LIMIT_SCRIPT.setLocation(new ClassPathResource("/lua/RateLimiterLua.lua")); // 设置脚本文件的位置
        LIMIT_SCRIPT.setResultType(Long.class); // 设置脚本执行的结果类型为Long
    }


    /**
     * 判断是否应该允许操作，基于固定窗口策略进行限流。
     *
     * @param key     用于标识操作的唯一键。
     * @param limit   在指定时间窗口内允许的操作最大次数。
     * @param timeout 时间窗口的时长。
     * @param unit    时间单位。
     * @return 如果允许操作返回true，否则返回false。
     */
    @Override
    public boolean shouldAllow(String key, int limit, long timeout, TimeUnit unit) {
        //将timeout转化为秒
        long time = TimeUnit.SECONDS.convert(timeout, unit);

        Long signal = stringRedisTemplate.execute(
                LIMIT_SCRIPT,
                Collections.singletonList(key),
                String.valueOf(limit),
                String.valueOf(time)
        );

        if(Objects.isNull(signal)){
            return false;
        }
        //返回0，说明被限流了
        return signal != 0;
    }


}